Skip to content

feat: two-phase CVM deploy support (prepareOnly + commitCvmUpdate)#217

Merged
Leechael merged 14 commits intomainfrom
feat/two-phase-deploy-multisig
Mar 22, 2026
Merged

feat: two-phase CVM deploy support (prepareOnly + commitCvmUpdate)#217
Leechael merged 14 commits intomainfrom
feat/two-phase-deploy-multisig

Conversation

@Leechael
Copy link
Collaborator

@Leechael Leechael commented Mar 20, 2026

Summary

Add multisig-friendly two-phase CVM deploy support to the JS SDK and CLI.

JS SDK

  • patchCvm: new prepareOnly option sends X-Prepare-Only: true header; 465 response now includes commitToken, commitUrl, apiCommitUrl
  • commitCvmUpdate: new action calling POST /cvms/{id}/commit-update with token-based auth (no API key required)

CLI

  • --prepare-only: run Phase 1 only, output compose hash + commit token info (JSON or human-readable)
  • --commit --token <t> --compose-hash <h> --transaction-hash <tx>: complete a previously prepared update (skips compose file reading)

Test Plan

  • cd js && bun run fmt && bun run lint && bun run type-check && bun run test — 666 passed
  • cd cli && bun run fmt && bun run lint && bun run type-check && bun run test — 403 passed

Manual Testing

# Prepare-only (requires on-chain KMS CVM)
phala deploy --cvm-id app_xxx --prepare-only -c docker-compose.yml
# → outputs compose_hash, commit_token, commit_url, api_commit_url

# Commit (after multisig approval + on-chain tx)
phala deploy --cvm-id app_xxx --commit \
  --token <commit-token> \
  --compose-hash <hash> \
  --transaction-hash <tx-hash>

…e action

- patchCvm: add prepareOnly option that sends X-Prepare-Only header,
  and include commitToken/commitUrl/apiCommitUrl in hash-required response
- New commitCvmUpdate action: POST /cvms/{id}/commit-update with token-based
  auth for completing multisig CVM updates
…ates

- --prepare-only: run Phase 1 only, output compose_hash + commit token info
- --commit: complete a prepared update using --token, --compose-hash, and
  --transaction-hash (skips compose file reading)
- Add usage examples for multisig workflows
@github-actions
Copy link
Contributor

github-actions bot commented Mar 20, 2026

📋 Check Results

✨ JS SDK - Code Formatting

Show format check results
✓ No formatting issues found

🔍 JS SDK - TypeScript Type Check

Show type check output
$ tsc --noEmit

🧪 JS SDK - Test Results

Show test output
$ vitest --run --exclude '**/*.e2e.test.ts'

�[7m�[1m�[36m RUN �[39m�[22m�[27m �[36mv1.6.1�[39m �[90m/home/runner/work/phala-cloud/phala-cloud/js�[39m

 �[32m✓�[39m src/client.test.ts �[2m (�[22m�[2m45 tests�[22m�[2m)�[22m�[90m 42�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/blockchains/deploy_app_auth.test.ts �[2m (�[22m�[2m27 tests�[22m�[2m)�[22m�[90m 50�[2mms�[22m�[39m
�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mStandard Version�[2m > �[22m�[2mshould add compose hash successfully with default parameters�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mStandard Version�[2m > �[22m�[2mshould handle custom timeout�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mStandard Version�[2m > �[22m�[2mshould use custom schema when provided�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mStandard Version�[2m > �[22m�[2mshould return raw data when schema is false�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mStandard Version�[2m > �[22m�[2mshould throw when custom schema validation fails�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mStandard Version�[2m > �[22m�[2mshould work with wallet client authentication�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mStandard Version�[2m > �[22m�[2mshould work with both clients provided�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mStandard Version�[2m > �[22m�[2mshould skip prerequisite checks when configured�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mStandard Version�[2m > �[22m�[2mshould use retry mechanism when enabled�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mStandard Version�[2m > �[22m�[2mshould handle progress callbacks�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mSafe Version�[2m > �[22m�[2mshould return success result when operation succeeds�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mSafe Version�[2m > �[22m�[2mshould work with custom schema�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mSafe Version�[2m > �[22m�[2mshould return raw data when schema is false�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mSafe Version�[2m > �[22m�[2mshould work without parameters�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mSafe Version�[2m > �[22m�[2mshould work with empty parameters object�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mSchema Flexibility�[2m > �[22m�[2mshould allow extra fields in transaction receipt for forward compatibility�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mSchema Flexibility�[2m > �[22m�[2mshould handle ComposeHashAdded event when present�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mType Inference�[2m > �[22m�[2mshould infer correct types for default schema�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mType Inference�[2m > �[22m�[2mshould infer correct types for custom schema�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mType Inference�[2m > �[22m�[2mshould infer unknown type when schema is false�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mSafe Version Type Inference�[2m > �[22m�[2mshould infer correct SafeResult types for default schema�[22m�[39m
[]

�[90mstdout�[2m | src/actions/blockchains/add_compose_hash.test.ts�[2m > �[22m�[2maddComposeHash�[2m > �[22m�[2mSafe Version Type Inference�[2m > �[22m�[2mshould infer correct SafeResult types for custom schema�[22m�[39m
[]

 �[32m✓�[39m src/actions/blockchains/add_compose_hash.test.ts �[2m (�[22m�[2m34 tests�[22m�[2m)�[22m�[90m 156�[2mms�[22m�[39m
 �[32m✓�[39m src/utils/define-action.test.ts �[2m (�[22m�[2m24 tests�[22m�[2m)�[22m�[90m 20�[2mms�[22m�[39m
 �[32m✓�[39m src/utils/define-action.type.test.ts �[2m (�[22m�[2m20 tests�[22m�[2m)�[22m�[90m 20�[2mms�[22m�[39m
 �[32m✓�[39m src/types/cvm_id.test.ts �[2m (�[22m�[2m53 tests�[22m�[2m)�[22m�[90m 37�[2mms�[22m�[39m
 �[32m✓�[39m src/utils/errors.test.ts �[2m (�[22m�[2m28 tests�[22m�[2m)�[22m�[90m 19�[2mms�[22m�[39m
 �[32m✓�[39m src/utils/hostname.test.ts �[2m (�[22m�[2m53 tests�[22m�[2m)�[22m�[90m 21�[2mms�[22m�[39m
 �[32m✓�[39m src/parse_dotenv.test.ts �[2m (�[22m�[2m71 tests�[22m�[2m)�[22m�[90m 26�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/provision_cvm_compose_file_update.test.ts �[2m (�[22m�[2m29 tests�[22m�[2m)�[22m�[90m 39�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/get_current_user.test.ts �[2m (�[22m�[2m14 tests�[22m�[2m)�[22m�[90m 27�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/commit_cvm_compose_file_update.test.ts �[2m (�[22m�[2m12 tests�[22m�[2m)�[22m�[90m 26�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/get_cvm_info.test.ts �[2m (�[22m�[2m11 tests�[22m�[2m)�[22m�[90m 29�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/watch_cvm_state.test.ts �[2m (�[22m�[2m6 tests�[22m�[2m)�[22m�[90m 62�[2mms�[22m�[39m
 �[32m✓�[39m src/version-inference.type.test.ts �[2m (�[22m�[2m17 tests�[22m�[2m)�[22m�[90m 81�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/get_cvm_compose_file.test.ts �[2m (�[22m�[2m10 tests�[22m�[2m)�[22m�[90m 19�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/provision_cvm.test.ts �[2m (�[22m�[2m14 tests�[22m�[2m)�[22m�[90m 16�[2mms�[22m�[39m
 �[32m✓�[39m src/client.extend.type.test.ts �[2m (�[22m�[2m8 tests�[22m�[2m)�[22m�[90m 5�[2mms�[22m�[39m
 �[32m✓�[39m src/version-inference.runtime.test.ts �[2m (�[22m�[2m9 tests�[22m�[2m)�[22m�[90m 14�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/get_cvm_list.test.ts �[2m (�[22m�[2m6 tests�[22m�[2m)�[22m�[90m 23�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/kms/get_app_env_encrypt_pubkey.test.ts �[2m (�[22m�[2m9 tests�[22m�[2m)�[22m�[90m 20�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/patch_cvm.test.ts �[2m (�[22m�[2m10 tests�[22m�[2m)�[22m�[90m 18�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/kms/get_kms_list.test.ts �[2m (�[22m�[2m8 tests�[22m�[2m)�[22m�[90m 19�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/commit_cvm_provision.test.ts �[2m (�[22m�[2m7 tests�[22m�[2m)�[22m�[90m 12�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/kms/get_kms_info.test.ts �[2m (�[22m�[2m8 tests�[22m�[2m)�[22m�[90m 19�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/get_cvm_stats.test.ts �[2m (�[22m�[2m8 tests�[22m�[2m)�[22m�[90m 16�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/confirm_cvm_patch.test.ts �[2m (�[22m�[2m9 tests�[22m�[2m)�[22m�[90m 16�[2mms�[22m�[39m
 �[32m✓�[39m src/client.extend.test.ts �[2m (�[22m�[2m6 tests�[22m�[2m)�[22m�[90m 16�[2mms�[22m�[39m
 �[32m✓�[39m src/utils/validate-parameters.test.ts �[2m (�[22m�[2m9 tests�[22m�[2m)�[22m�[90m 7�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/get_available_nodes.test.ts �[2m (�[22m�[2m6 tests�[22m�[2m)�[22m�[90m 16�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/ssh_keys/create_ssh_key.test.ts �[2m (�[22m�[2m9 tests�[22m�[2m)�[22m�[90m 15�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/shutdown_cvm.test.ts �[2m (�[22m�[2m8 tests�[22m�[2m)�[22m�[90m 16�[2mms�[22m�[39m
 �[32m✓�[39m src/types/app_compose.test.ts �[2m (�[22m�[2m9 tests�[22m�[2m)�[22m�[90m 14�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/restart_cvm.test.ts �[2m (�[22m�[2m8 tests�[22m�[2m)�[22m�[90m 16�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/start_cvm.test.ts �[2m (�[22m�[2m8 tests�[22m�[2m)�[22m�[90m 20�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/stop_cvm.test.ts �[2m (�[22m�[2m8 tests�[22m�[2m)�[22m�[90m 14�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/get_cvm_containers_stats.test.ts �[2m (�[22m�[2m6 tests�[22m�[2m)�[22m�[90m 17�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/ssh_keys/delete_ssh_key.test.ts �[2m (�[22m�[2m8 tests�[22m�[2m)�[22m�[90m 13�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/ssh_keys/sync_github_ssh_keys.test.ts �[2m (�[22m�[2m6 tests�[22m�[2m)�[22m�[90m 12�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/delete_cvm.test.ts �[2m (�[22m�[2m8 tests�[22m�[2m)�[22m�[90m 15�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/get_cvm_state.test.ts �[2m (�[22m�[2m4 tests�[22m�[2m)�[22m�[90m 12�[2mms�[22m�[39m
 �[32m✓�[39m src/utils/as-hex.test.ts �[2m (�[22m�[2m9 tests�[22m�[2m)�[22m�[90m 6�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/refresh_cvm_instance_ids.test.ts �[2m (�[22m�[2m2 tests�[22m�[2m)�[22m�[90m 7�[2mms�[22m�[39m
 �[32m✓�[39m src/actions/cvms/refresh_cvm_instance_id.test.ts �[2m (�[22m�[2m2 tests�[22m�[2m)�[22m�[90m 7�[2mms�[22m�[39m

�[2m Test Files �[22m �[1m�[32m44 passed�[39m�[22m�[90m (44)�[39m
�[2m      Tests �[22m �[1m�[32m666 passed�[39m�[22m�[90m (666)�[39m
�[2m   Start at �[22m 14:03:28
�[2m   Duration �[22m 8.92s�[2m (transform 1.16s, setup 4ms, collect 11.41s, tests 1.07s, environment 12ms, prepare 5.62s)�[22m

📝 JS SDK - Lint Check

Show lint results
✓ No linting issues found

🌐 JS SDK - Browser Compatibility

Show browser test results

🌐 Browser Compatibility Report

Browser compatibility tests completed across:

  • ✓ Chromium
  • ✓ Firefox
  • ✓ WebKit (Safari)

The SDK has been verified to work in modern browser environments.


Check run: https://github.com/Phala-Network/phala-cloud/actions/runs/23404685997

Also add webhook integration guide to docs covering:
- Payload format and supported events
- HMAC-SHA256 signature verification (Python + Node.js examples)
- Delivery behavior: 2xx success, no redirect following, 10s timeout
- Retry policy: 4 attempts (1min, 10min, 1h intervals)
- Best practices for idempotency, replay protection, and HTTPS
…hain CVM

Without the return, execution fell through to the generic success
output, producing double JSON output that breaks consuming scripts.
Leechael added 10 commits March 21, 2026 11:54
The commit-update endpoint is token-based (no auth needed). Previously
getApiClient threw if no API key was configured, blocking signers who
only have a commit token.
dedent template literal was mangling backslash continuations.
Build the commit command separately with explicit join.
Display chain name, chain ID, and contract explorer link when using
--prepare-only. Also note that API Commit URL uses POST method.
Compose hash is now displayed as 0x-prefixed hex in both human-readable
and JSON output for direct copy-paste compatibility.
…tting

fix: CLI --prepare-only output formatting
…-info

feat: CLI --prepare-only shows chain ID and block explorer
fix: CLI compose hash 0x prefix in --prepare-only
- Contract explorer URL: 0x prefix on contract address
- Expired/invalid token error: suggest running --prepare-only again
Show compose hash and device ID registration status from on-chain check.
When all prerequisites are met, suggest using --transaction-hash already-registered.
Also add expired token hint suggesting --prepare-only re-run.
…ash optional

- Replace dedent template literal with line-by-line array join for
  consistent output formatting without indentation artifacts
- --transaction-hash is now optional in --commit mode; defaults to
  'already-registered' (state-only check) with info message
- --compose-hash also optional (server reads from token)
@Leechael Leechael merged commit 20a1652 into main Mar 22, 2026
12 checks passed
@Leechael Leechael deleted the feat/two-phase-deploy-multisig branch March 22, 2026 14:43
@Leechael Leechael restored the feat/two-phase-deploy-multisig branch March 23, 2026 01:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants